const path = require('path')
const fs = require('fs-extra')
const { pipeStream } = require('../utils')

const { randomBytes } = require('node:crypto')


// 临时文件夹 - 放在项目根目录
const tempDir = path.resolve(__dirname, '../..', 'temp')

// 大文件上传成功后，合成以后，文件会放到这里， 上面放分片文件数据
const uploadDir = path.resolve(__dirname, '../..', 'uploads')


/**
 * 基础文件上传
 * @param {import('express').Request} req
 * @param {import('express').Response} res
 */
const baseUpload = async (req, res) => {
  // 确保临时文件夹存在(如果不存在，则创建)
  await fs.ensureDir(tempDir)

  // 获取文件名 - express路由的动态参数 /:filename
  const { filename } = req.params

  console.log('前端传递的文件名');
  console.log(filename);

  // 解析文件后缀
  let ext = path.parse(filename).ext

  // 这里文件名是后台生成，后续改为前端根据文件hash计算生成，相同文件可以得到同样的，好判定文件是否已经上传过
  let renameFile = generateFileName() + ext

  // 拼接完整的路径
  const filePath = path.resolve(tempDir, renameFile)

  // 创建可写流
  const ws = fs.createWriteStream(filePath)

  // 以流的形式-写入文件 - 注意前端要设置 Content-Type: "application/octet-stream"
  await pipeStream(req, ws)

  res.json({
    code: 0,
    data: {
      path: `http://localhost:3003/temp/${renameFile}`,
      ext: ext
    },
    message: '操作成功'
  })
}

/**
 * 大文件切片上传
 * @param {import('express').Request} req
 * @param {import('express').Response} res
 */
const bigFileCutUpload = async (req, res) => {
  // 确保2个文件夹都存在
  await fs.ensureDir(uploadDir)
  await fs.ensureDir(uploadDir)

  // 获取文件名 - 这里一定存在，否则路由无法匹配 属于 express的动态路由
  const { filename } = req.params


  // 切片名 - 这里名字要和前端传递的url参数保持一致，包括大小写，否则接收不到
  let { chunkFilename, start } = req.query

  start = isNaN(Number(start)) ? 0 : Number(start)

  if (!chunkFilename) {
    res.json({
      code: -1,
      data: null,
      message: '请传递切片名【chunkFilename】'
    })
    return
  }

  // 拼接切片文件夹路径 - 因为切片是一个跟着一个，都丢到临时文件夹的
  const chunkDir = path.resolve(tempDir, filename)

  // 确保切片文件夹存在 - 如果不存，则创建
  // 基于某一个文件名创建一个单独的子目录，比如 前端上传yoyo.mp4, 那么创建一个 temp/yoyo 这么一个目录，把所有切片都放在这里
  // 到时候合并的时候，只要合并整个文件夹里面的内容即可，不能不同文件的切片都丢到temp根目录，后面合成的时候就不好操作-还得筛选文件
  await fs.ensureDir(chunkDir)

  const chunkPath = path.resolve(chunkDir, chunkFilename)

  // 创建一个可写流
  const ws = fs.createWriteStream(chunkPath, {
    flags:'a',
    // 注意： 如果是续传，一定要前端告诉后台，从哪个地方开始写入文件流
    start
  })

  // 监听请求取消的事件已经要先监听，不能放在下面写这个动作的后面，否则永远无法生效
  req.on('aborted', () => {
    console.log('请求已取消');
    ws.close()
  })

  await pipeStream(req, ws)


  res.json({
    code: 0,
    data: null,
    message: '操作成功'
  })
}

/**
 * 分片文件合并
 * @param {import('express').Request} req
 * @param {import('express').Response} res
 */
const mergeBigFile = async (req, res) => {
  // 获取文件名
  const { filename } = req.params

  // console.log('前端传递要合并的文件名是');
  // console.log(filename);

  // 拼接切片文件夹路径
  const chunkDir = path.resolve(tempDir, filename)

  // 确认文件夹是否存在
  try {
    await fs.access(chunkDir)
  } catch (error) {
    res.json({
      code: -1,
      data: null,
      message: '切片文件不存在'
    })
    return
  }

  const chunks = await fs.readdir(chunkDir)

  if (chunks.length === 0) {
    res.json({
      code: -1,
      data: null,
      message: '切片文件不存在'
    })
    return
  }

  // 按照切片的索引进行排序 - 切片在上传的时候，单个切片文件名是有顺序标记的，第一位就是
  chunks.sort((a, b) => {
    return a.split('-')[0] - b.split('-')[0]
  })

  // 文件名是带原始文件的 ext 文件类型的，所以不用单独处理-也可以前端上传文件后缀
  const uploadsPath = path.resolve(uploadDir, filename)

  console.log('最终写入的文件地址');
  console.log(uploadsPath);
  // console.log(chunks);

  // 这里的chunkSize要跟前端定义的一致， 否则文件在追加的时候索引位置会错误，导致文件合成失败
  const chunkSize = isNaN(Number(req.headers['chunk-size'])) ? 20 * 1024 * 1024 : Number(req.headers['chunk-size'])

  const writeTasks = chunks.map((item, index) => {
    // 拿到当前切片的路径
    const chunkPath = path.resolve(chunkDir, item)

    // 创建可读-把单个分片的内容读取到-以流的形式，再用下面的可写流写入（追加的形式）
    const rs = fs.createReadStream(chunkPath)

    // 创建可写流
    const ws = fs.createWriteStream(uploadsPath, {
      // flags:'a+',
      // 追加写入
      // 这里 使用 a这个标志，在window下合成以后的文件是损坏的，直接不添加flags
      // @link https://nodejs.org/docs/latest/api/fs.html#file-system-flags
      start: index * chunkSize
    })

    return pipeStream(rs, ws)
  })

  await Promise.all(writeTasks)

  // 合并完成以后，删除切片文件夹
  await fs.rm(chunkDir, {
    recursive: true,
    force: true
  })

  // 在main.js中已经将 uploads设置为 静态目录，路径可以直接以url形式访问
  res.json({
    code: 0,
    data: `http://localhost:3003/uploads/` + filename,
    message: '文件切片合并成功'
  })
}

/**
 * 根据文件hash，验证文件是否已经存在
 * @param {import('express').Request} req
 * @param {import('express').Response} res
 *
 * 1. 服务器没有这个文件，则采用切片上传
 * 2. 服务有这个文件的部分切片
 *  2.1 部分切片是完整的，则只需要上传剩下切片即可
 *  2.2 部分切片不完整，则不完整的切片还要继续上传，只上传不完整的部分二进制数据即可
 */
const verifyBigFile = async (req, res) => {

  const { filename } = req.params

  const filePath = path.resolve(uploadDir, filename)
  // 判断文件是否存在
  const isExit = await fs.pathExists(filePath)

  if (isExit) {
    res.json({
      code: 0,
      data: `http://localhost:3003/uploads/${filename}`,
      message: '文件已存在, 请直接使用'
    })
    return
  }

  // 文件不存在 = 文件是半成品上传了 或者文件直接没有上传

  const chunkDir = path.resolve(tempDir, filename)

  // 判断切片文件夹是否存在
  const hasChunks = await fs.pathExists(chunkDir)


  let uploadedChunks = []

  if (hasChunks) {
    // 读取已经上传的切片的大小

    const chunkFilenames = await fs.readdir(chunkDir)

    const getChunks = () => {
      return chunkFilenames.map(async (item) => {
        let itemPath = path.resolve(chunkDir, item)

        const { size } = await fs.stat(itemPath)

        return {
          size,
          chunkFilename: item
        }
      })
    }

    uploadedChunks = await Promise.all(getChunks())

    res.json({
      code: 0,
      data: uploadedChunks,
      needUpload: true,
      message: '存在部分分片,需要上传'
    })

    return
  }


  res.json({
    code: 0,
    data: [],
    needUpload: true,
    message: '文件不存在，需要重新上传'
  })

}


const generateFileName = (file) => {
  let buf = randomBytes(16)

  // 生成一个文件名
  return buf.toString('hex')
}


module.exports = {
  baseUpload,
  bigFileCutUpload,
  mergeBigFile,
  verifyBigFile
}